﻿/*
 * Copyright 2011 Edward L. Stauff.                     Contact: <EdStauff@gmail.com>.
 * 
 * This file, "HelpParam.cs", is part of the "Stauffware.Common.Console" class 
 * library.  "Stauffware.Common" is free software: you can redistribute it and/or 
 * modify it under the terms of the GNU Lesser Public License as published by the 
 * Free Software Foundation, either version 3 of the License, or (at your option) 
 * any later version.
 * 
 * "Stauffware.Common" is distributed in the hope that it will be useful, 
 * but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
 * or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser Public License for more 
 * details.
 * 
 * You should have received a copy of the GNU Lesser Public License along with 
 * "Stauffware.Common".  If not, see <http://www.gnu.org/licenses/>.
 */
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Reflection;
using Stauffware.Common.Core;

namespace Stauffware.Common.Console
{
	/// <summary>
	/// The class that handles command-line "help" parameters.
	/// </summary>
	internal class HelpParam : CmdLineParam
	{
		public override bool HasValue { get { return false; } }

		// a list of the parameter value types to display help for
		private HashSet<Type> ParamValueTypes;

	//	private string AppName;

		//-----------------------------------------------------------------------------
		/// <summary>
		/// constructor
		/// </summary>

		public HelpParam (CmdLineParamGroup group, string name)
			: base(name, group, typeof(string))
		{
			this.Arity = Arity.Multiple;
			this.Position = 0;
			this.IsRequired = false;
			this.Name = name;
			this.Help = "Displays help.  You can optionally specify one or more parameter names to get help on.";
		}

		//-----------------------------------------------------------------------------

		internal override int ProcessArgs (string actualName, ArgValueList argValues)
		{
			this.ParamValueTypes = new HashSet<Type>();

			TextWriter tw = this.CmdLineParser.HelpWriter;

			int width = 80;
#if !SILVERLIGHT
			try { width = System.Console.WindowWidth; }
			catch (Exception x)
			{
				ErrorUtils.DumpExceptionChain(x, tw);
			}
#endif

			tw = new FormattingTextWriter(tw, (uint) width);

			int argsProcessed = argValues.Remaining;

			if (argValues.Remaining == 0)
				this.OutputAllHelp(tw);
			else this.OutputSelectedHelp(argValues, tw);

			if (this.CmdLineParser.HelpExitCode != null)
#if SILVERLIGHT
				throw new QuitApplicationException();
#else
				Environment.Exit(this.CmdLineParser.HelpExitCode.Value);
#endif
			return argsProcessed;
		}

		//-----------------------------------------------------------------------------

		private void OutputAllHelp (TextWriter tw)
		{
			tw.WriteLine();

#if SILVERLIGHT
			Assembly asm = Assembly.GetExecutingAssembly();
#else
			Assembly asm = Assembly.GetEntryAssembly();
#endif
			if (asm == null)	// can happen if called from unmanaged code, as happens with NUnit
				asm = Assembly.GetCallingAssembly();

			AssemblyName name = asm.GetName();
			
			if (ConsoleApp.Current == null)		// ConsoleApp does this also; don't want two
				tw.WriteLine(name.Name + "            version " + name.Version);

			if (!string.IsNullOrEmpty(this.CmdLineParser.ApplicationHelp))
				tw.WriteLine("\n" + this.CmdLineParser.ApplicationHelp.Trim());

			if (this.CmdLineParser.PositionalParams.Count > 0)
			{
				this.OutputSectionHeading("Positional Parameters", tw);
				tw.WriteLine("You can provide these parameter values without parameter names, " + 
							 "but they must appear in the order specified.  " +
							 "Parameters following '*' are optional, but if you omit one, " + 
							 "you must omit the ones after it as well.", tw);
				tw.Write("\n    " + name.Name);
				bool wroteOpt = false;

				foreach (CmdLineParam p in this.CmdLineParser.PositionalParams)
				{
					if (!p.IsRequired && !wroteOpt)
					{
						tw.Write(" *");
						wroteOpt = true;
					}
					tw.Write(" <" + p.Name + ">");
				}

				tw.WriteLine();
			}

			this.OutputSectionHeading("Named Parameters", tw);
			tw.Write("(Parameter name case is ignored.  " + 
						 "You can abbreviate a parameter ");
			if (this.CmdLineParser.NamedParamGroups.Count > 0)
				tw.Write("or group ");
			tw.WriteLine("name to any unambiguous prefix.  " + 
						 "Named parameters can appear in any order.)");

			foreach (CmdLineParam p in this.CmdLineParser.UnscopedParams.Values)
				this.OutputParamHelp(p, tw);
			foreach (CmdLineParamGroup g in this.CmdLineParser.NamedParamGroups.Values)
				this.OutputGroupHelp(g, tw);

			this.OutputParamTypeHelp(tw);

			if (AppExitCode.AllExitCodes.Count > 0)
			{
				this.OutputSectionHeading("Exit Codes:", tw);
				foreach (AppExitCode x in AppExitCode.AllExitCodes.Values)
					tw.WriteLine(x.Value.ToString() + " = " + x.Description);
			}
		}

		//-----------------------------------------------------------------------------

		private void OutputSelectedHelp (ArgValueList argValues, TextWriter tw)
		{
			for ( ; argValues.Remaining > 0; argValues.Next())
			{
				CmdLineParam clp;
				CmdLineSyntaxError err = this.CmdLineParser.FindParam(argValues[0], out clp);
				if (err == null)
				{
					this.OutputParamHelp(clp, tw);
					continue;
				}
				else
				{
					CmdLineParamGroup grp;
					PrefixMatch pm = this.CmdLineParser.NamedParamGroups.Lookup(argValues[0], out grp);
					if (pm == PrefixMatch.Complete || pm == PrefixMatch.Partial)
					{
						this.OutputGroupHelp(grp, tw);
						continue;
					}
				}

				tw.WriteLine("\n" + argValues[0] + " : ambiguous or undefined parameter or group name");
			}
		}

		//-----------------------------------------------------------------------------

		private void OutputGroupHelp (CmdLineParamGroup group, TextWriter sw)
		{
			if (!string.IsNullOrEmpty(group.GroupName))
			{
				sw.WriteLine();
				sw.WriteLine("== " + group.GroupName + " ==");
			}

			if (!string.IsNullOrEmpty(group.GroupHelp))
			{
				sw.WriteLine();
				sw.WriteLine(group.GroupHelp);
			}

			foreach (CmdLineParam p in group.AllParams.Values)
				this.OutputParamHelp(p, sw);
		}

		//-----------------------------------------------------------------------------

		private void OutputParamHelp (CmdLineParam clp, TextWriter tw)
		{
			tw.WriteLine();
			tw.Write(clp.FullName);
			
			if (clp.Arity != Arity.None)
			{
				if (clp.EffectiveValueType != null)
					tw.Write("  <" + clp.EffectiveValueType.Name + ">");
				else tw.Write("  <?!?>");
				if (clp.Arity == Arity.Multiple)
					tw.Write("...");
				this.ParamValueTypes.Add(clp.EffectiveValueType);
			}
			
			if (clp.IsRequired)
				tw.Write("     [REQUIRED]");
			tw.WriteLine();

			if (string.IsNullOrEmpty(clp.Help))
				tw.WriteLine("    (No help available.  Report this as a bug.)");
			else tw.WriteLine("    " + clp.Help);

			if (clp.EffectiveValueType != null && clp.Arity != Arity.None)
			{
				// If this is the only parameter that uses this enum parameter type, then
				// list the values and their help here.  If there are multiple parameters
				// of this type, then we'll list the values under Parameter Value Syntax.
				if (clp.EffectiveValueType.IsEnum /* && (this.CmdLineParser.CountParams(clp.ValueType) <= 1) */)
				{
					string prologue;

					if (clp.EffectiveValueType.HasFlagsAttribute())
						prologue = "    One or more (separated by '+') of:";
					else prologue = "    One of:";

					OutputEnumValueHelp(clp.EffectiveValueType, prologue, ", ", "", tw);
				}

				if (clp.HasValue && !clp.IsRequired)
						tw.WriteLine("    Default value: " + clp.GetValueString());
			}
		}

		//-----------------------------------------------------------------------------

		private void OutputParamTypeHelp (TextWriter tw)
		{
			if (this.ParamValueTypes.Count == 0)
				return;

			this.OutputSectionHeading("Parameter Value Syntax", tw);

			int maxNameLen = 0;
			foreach (ICmdLineValueParser clvp in this.CmdLineParser.ValueParserFactory.GetAll())
				maxNameLen = Math.Max(maxNameLen, clvp.ValueType.Name.Length);

			foreach (ICmdLineValueParser clvp in this.CmdLineParser.ValueParserFactory.GetAll())
			{
				if (clvp.ValueType.IsEnum && (this.CmdLineParser.CountParams(clvp.ValueType) <= 1))
					continue;

				if (clvp.ValueType.IsEnum)
				{
					tw.Write(clvp.ValueType.Name.PadRight(maxNameLen) + " := ");
					if (!string.IsNullOrEmpty(clvp.Help))
						tw.WriteLine(clvp.Help);
					else
					{
						string prologue, epilogue;
						if (clvp.ValueType.HasFlagsAttribute())
						{
							prologue = "( ";
							epilogue = " ) [\"+\" " + clvp.ValueType.Name + "]...";
						}
						else
						{
							prologue = "";
							epilogue = "";
						}
					//"(use one or more) ");
					//+ help);
						OutputEnumValueHelp(clvp.ValueType, prologue, " | ", epilogue, tw);
					}
				}
				else
				{
					string help = clvp.Help;
					if (string.IsNullOrEmpty(help))
						help = "{ no help available; report this as a bug }";
					tw.WriteLine(clvp.ValueType.Name.PadRight(maxNameLen) + " := " + help);
				}
			}
		}

		//-----------------------------------------------------------------------------

		internal static void OutputEnumValueHelp (Type enumType, string prologue, 
									string separator, string epilogue, TextWriter tw)
		{
			Require.Condition(enumType.IsEnum, "type " + enumType.FullName + " is not an enum type");

			HelpAttribute ha;

			//HelpAttribute ha = enumType.GetCustomAttribute<HelpAttribute>(false);
			
			//if (ha != null && !string.IsNullOrEmpty(ha.Content))
			//    tw.WriteLine("    " + ha.Content);
			// tw.Write("    Values");
			//if (enumType.HasFlagsAttribute())
			//	tw.Write(" (use one or more)");

			FieldInfo[] values = enumType.GetFields();

			// see if any values have help
			bool anyHelp = false;
			foreach (FieldInfo value in values)
			{
				ha = value.GetCustomAttribute<HelpAttribute>(false);
				anyHelp |= (ha != null && !string.IsNullOrEmpty(ha.Content));
			}

			if (anyHelp)
			{
				tw.WriteLine(prologue);
				foreach (FieldInfo value in values)
				{
					if (value.IsSpecialName) continue;
					ha = value.GetCustomAttribute<HelpAttribute>(false);
					string help = (ha != null && ha.Content != null) ? ha.Content : "";
					tw.WriteLine("    \"" + value.Name + "\" " + Chars.EmDash.Unicode + " " + help);
				}
				tw.WriteLine(epilogue);
			}
			else
			{
				ListString ls = new ListString(separator);
				foreach (FieldInfo value in values)
					if (!value.IsSpecialName)
						ls.Add("\"" + value.Name + "\"");
				tw.WriteLine(prologue + ls.ToString() + epilogue);
			}
		}

		//-----------------------------------------------------------------------------

		internal static void WriteEnumHelp (Type enumType, TextWriter tw)
		{

		}

		//-----------------------------------------------------------------------------

		private void OutputSectionHeading (string name, TextWriter tw)
		{
			tw.WriteLine("\n" + name + "\n" + "".PadRight(name.Length, '-'));
		}

		//-----------------------------------------------------------------------------

		internal override string GetValueString ()
		{
			Assert.NotReached();	// why are we calling this on a Help parameter?!?
			return null;	// shut compiler up
		}

		internal override void SetValue (object value) // , object targetObj)
		{
			Assert.NotReached();	// why are we calling this on a Help parameter?!?
		}

		//-----------------------------------------------------------------------------
		/// <summary>
		/// Don't log help params.
		/// </summary>

		public override void Log (TextWriter tw, string linePrefix=null)
		{
		}
	}
}
